/*******************************************************************************
 * Copyright (c) 2014 Greg Marut.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *    Greg Marut - initial API and implementation
 ******************************************************************************/
package com.gregmarut.network.tcp.secure;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import com.gregmarut.network.tcp.ConnectedServer;
import com.gregmarut.network.tcp.GenericSocket;
import com.gregmarut.network.tcp.ListeningServer;
import com.gregmarut.network.tcp.ServerListener;
import com.gregmarut.util.ByteArray;
import com.gregmarut.util.Log;
import com.gregmarut.util.encryption.MD5;

/**
 * Provides the exact same functionality as a ListeningServer but also provides added 
 * security by encrypting data that is transmitted over the sockets.
 * 
 * @author Greg Marut
 */
public abstract class SecureListeningServer<L, S> extends ListeningServer<L, S>
{
	//** Finals **//
	//holds the algorithm to use for this encryption
	public static final String ALGORITHM = "PBEWithMD5AndDES";
	public static final int ITERATION_COUNT = 1024;
	public static final int SALT_LENGTH = 8;
	
	//** Objects **//
	//holds the cipher object responsible for encrypting and decrypting the data
	private final Cipher encryption;
	private final Cipher decryption;
	
	/**
	 * The constructor for SecureListeningServer
	 * @param port
	 * @param encryptionKey
	 */
	public SecureListeningServer(final String encryptionKey) throws InvalidEncryptionException
	{
		this(encryptionKey.getBytes());
	}
	
	/**
	 * The constructor for SecureListeningServer
	 * @param port
	 * @param strEncryptionKey
	 */
	public SecureListeningServer(final byte[] encryptionKey) throws InvalidEncryptionException
	{
		try
		{
			//create the secret key
			byte[] salt = new ByteArray(MD5.encrypt(encryptionKey)).subArray(0, SALT_LENGTH).getBytes();
			PBEParameterSpec specParams = new PBEParameterSpec(salt,	ITERATION_COUNT);
			KeySpec keySpec = new PBEKeySpec(new String(encryptionKey).toCharArray());
			SecretKey key = SecretKeyFactory.getInstance(ALGORITHM).generateSecret(keySpec); 
			
			//create the cipher object for encryption
			encryption = Cipher.getInstance(ALGORITHM);
			encryption.init(Cipher.ENCRYPT_MODE, key, specParams);
			
			//create the cipher object for encryption
			decryption = Cipher.getInstance(ALGORITHM);
			decryption.init(Cipher.DECRYPT_MODE, key, specParams);
		}
		catch(InvalidKeyException e)
		{
			// make sure the logger should display this error
			if (Log.DISPLAY_INTERNAL_ERRORS)
			{
				// log the error
				Log.err(e);
			}
			
			//throw a new invalid encryption exception
			throw new InvalidEncryptionException(e);
		}
		catch (InvalidKeySpecException e)
		{
			// make sure the logger should display this error
			if (Log.DISPLAY_INTERNAL_ERRORS)
			{
				// log the error
				Log.err(e);
			}
			
			//throw a new invalid encryption exception
			throw new InvalidEncryptionException(e);
		}
		catch (NoSuchAlgorithmException e)
		{
			// make sure the logger should display this error
			if (Log.DISPLAY_INTERNAL_ERRORS)
			{
				// log the error
				Log.err(e);
			}
			
			//throw a new invalid encryption exception
			throw new InvalidEncryptionException(e);
		}
		catch (NoSuchPaddingException e)
		{
			// make sure the logger should display this error
			if (Log.DISPLAY_INTERNAL_ERRORS)
			{
				// log the error
				Log.err(e);
			}
			
			//throw a new invalid encryption exception
			throw new InvalidEncryptionException(e);
		}
		catch (InvalidAlgorithmParameterException e)
		{
			// make sure the logger should display this error
			if (Log.DISPLAY_INTERNAL_ERRORS)
			{
				// log the error
				Log.err(e);
			}
			
			//throw a new invalid encryption exception
			throw new InvalidEncryptionException(e);
		}
	}
	
	/**
	 * Returns the server activity to use for this server
	 * @return
	 */
	protected ServerActivity getServerActivity()
	{
		return new ServerActivity();
	}
	
	/**
	 * A class used to handle the listening process of the server
	 * 
	 * @author Greg Marut
	 */
	protected class ServerActivity extends ListeningServer<L, S>.ServerActivity
	{
		@Override
		protected ConnectedServer<S> createNewConnectedServer(GenericSocket<S> socket)
		{
			//return a new instance of a connected server
			return new SecureConnectedServer<S>(socket, 
				new ArrayList<ServerListener>(serverListeners), 
				encryption, decryption);
		}
	}
}
